#! python
"""
################################################################################
一个 Python/tkinter 计算器程序和GUI部件。

在表达式输入时对其求职，捕获通过键盘进行的表达式输入；2.0里添加了整合的命令行弹出窗口特性，最近计算历史
显示弹出窗、字体和颜色配置、帮助和关于弹出窗、预导入math/random常量等；

这个计算器有个毛病，用eval可以正常计算，用=计算最后结果，第一次可以，但多按几下就变成ERROR了

要清除，每次按下操作符才会把此操作符之前的操作数判断压入操作数堆栈或计算之前的式子并放入操作数堆栈
################################################################################
"""

from tkinter import *                                                       # 组件、常量
import sys, os
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
__package__ = os.path.split(os.path.dirname(__file__))[-1]
from .涉及tkinter的包.gui混合类_guimixin import GuiMixin                       # quit方法
from .涉及tkinter的包.组件构建器_widgets import label, entry, button, frame     # 组件构建器
Fg, Bg, Font = 'black', 'skyblue', ('courier', 14, 'bold')                  # 默认配置

debugme = True            # 设为真后，每次Evaluator类将要应用操作符并缩短（弹出）堆栈的时候，stdout上都会显示操作符和操作数堆栈


def trace(*args):
    if debugme:
        print(args)


################################################################################
# 主类：处理用户界面；
# 一个经过扩展的Frame，或用在新的Toplevel上，或嵌入另一个容器组件
################################################################################

class CalcGui(GuiMixin, Frame):
    """
    这个类管理GUI本身。控制输入事件并负责主窗口顶部的显示区域。不过不进行表达式求值；
    这方面只负责把输入GUI的操作符和操作数发送到内嵌的Evaluator类实例。
    """
    Operators = "+-*/="                                 # 按钮显示的操作符列表
    Operands = ["abcd", "0123", "4567", "89()"]         # 可以定制的操作数等列表

    def __init__(self, parent=None, fg=Fg, bg=Bg, font=Font):
        Frame.__init__(self, parent)
        self.pack(expand=YES, fill=BOTH)             # 所有部分均可扩展
        self.eval = Evaluator()                      # 嵌入一个堆栈处理器实例
        self.text = StringVar()                      # 创建一个链接好的文本变量
        self.text.set("0")
        self.erase = 1                               # 接下来清除文本“0”
        self.makeWidgets(fg, bg, font)               # 构建GUI本身
        if not parent or not isinstance(parent, Frame):
            self.master.title('PyCalc-计算器')          # 当且仅当拥有窗口时的标题
            self.master.iconname("PyCalc")           # 键绑定也是一样
            self.master.bind('<KeyPress>', self.onKeyboard)     # 可以用键盘实现运算，比如键盘输入5*4再按回车就可以出结果
            self.entry.config(state='readonly')      # 不再是'disabled'代表灰色不可用
        else:
            self.entry.config(state='normal')
            self.entry.focus()

    def makeWidgets(self, fg, bg, font):             # 7个frame框架加上文本输入框
        self.entry = entry(self, TOP, self.text)     # 字体、颜色均可配置
        self.entry.config(font=font)                 # 让字体显示再大一些
        self.entry.config(justify=RIGHT)             # 变成右对齐
        for row in self.Operands:
            frm = frame(self, TOP)
            for char in row:
                button(frm, LEFT, char,
                            lambda op=char: self.onOperand(op),
                            fg=fg, bg=bg, font=font)

        frm = frame(self, TOP)
        for char in self.Operators:
            # 按照前面已经给出的self.Operators的+-*/=依次做成按钮，并绑定按下的lambda方法λ入
            button(frm, LEFT, char,
                        lambda op=char: self.onOperator(op),
                        fg=bg, bg=fg, font=font)

        frm = frame(self, TOP)
        button(frm, LEFT, 'dot ', lambda: self.onOperand('.'))
        button(frm, LEFT, ' E+ ', lambda: self.text.set(self.text.get()+'E+'))
        button(frm, LEFT, ' E- ', lambda: self.text.set(self.text.get()+'E-'))
        button(frm, LEFT, 'cmd ', self.onMakeCmdline)
        button(frm, LEFT, 'help', self.help)
        button(frm, LEFT, 'quit', self.quit)        # 来自gui混合类

        frm = frame(self, BOTTOM)
        button(frm, LEFT, 'eval ', self.onEval)
        button(frm, LEFT, 'hist ', self.onHist)
        button(frm, LEFT, 'clear', self.onClear)

    def onClear(self):
        """按下clear时候产生的反应"""
        self.eval.clear()
        self.text.set('0')
        self.erase = 1

    def onEval(self):
        """按下eval时候产生的反应"""
        self.eval.shiftOpnd(self.text.get())        # 最后的或仅有一个操作数
        self.eval.closeall()                        # 应用余下的所有操作符
        self.text.set(self.eval.popOpnd())          # 必须弹出：下一个操作符？
        self.erase = 1

    def onOperand(self, char):
        """按下操作数后反应"""
        # (,)也被当成操作数对待
        if char == '(':
            self.eval.open()
            self.text.set('(')                      # 清除接下来的文本
            self.erase = 1
        elif char == ')':
            self.eval.shiftOpnd(self.text.get())    # 最后的或仅有一个嵌套的操作数
            self.eval.close()                       # 这里也必须弹出：下一个操作符？
            self.text.set(self.eval.popOpnd())
            self.erase = 1
        else:
            if self.erase:
                self.text.set(char)                     # 清除前一个值，或者说覆盖，避免产生影响。比如刚开始显示的是0，按下5后，则变成5
            else:
                self.text.set(self.text.get() + char)   # 否则追加到操作数尾部
            self.erase = 0

    def onOperator(self, char):
        """按下操作符时候的反应"""
        self.eval.shiftOpnd(self.text.get())            # 调用Evaluator实例，推送左侧的操作数
        self.eval.shiftOptr(char)                       # 是否对左侧的表达式求值？
        self.text.set(self.eval.topOpnd())              # 推送操作符，显示操作数|结果
        self.erase = 1                                  # 在操作数|'('输入时擦除掉

    def onMakeCmdline(self):
        new = Toplevel()                                # 新的顶级窗口
        new.title('PyCalc command line')                # 自定的Python代码
        frm = frame(new, TOP)                           # 只扩展输入框
        label(frm, LEFT, '>>>').pack(expand=NO)
        var = StringVar()
        ent = entry(frm, LEFT, var, width=40)
        onButton = (lambda: self.onCmdline(var, ent))
        onReturn = (lambda event: self.onCmdline(var, ent))
        button(frm, RIGHT, 'Run', onButton).pack(expand=NO)
        ent.bind('<Return>', onReturn)
        var.set(self.text.get())

    def onCmdline(self, var, ent):                      # 对命令行弹出窗口的输入做求值
        """按下cmd按钮的反应"""
        try:
            value = self.eval.runstring(var.get())
            var.set('OKAY')
            if value is not None:                       # 在求值命名空间字典中进行
                self.text.set(value)                    # 表达式或语句
                self.erase = 1
                var.set('OKAY => ' + value)
        except:                                         # 作用于计算区域
            var.set('ERROR')                            # 弹出窗口区域的状态
        ent.icursor(END)                                # 文本后插入小数点
        ent.select_range(0, END)                        # 选择下一个按键行为删除的消息

    def onKeyboard(self, event):
        pressed = event.char                            # 发生键盘按下的事件时
        if pressed != '':                               # 假装按下了按钮
            if pressed in self.Operators:
                self.onOperator(pressed)
            else:
                for row in self.Operands:
                    if pressed in row:
                        self.onOperand(pressed)
                        break
                else:
                    if pressed == '.':
                        self.onOperand(pressed)                # 可以开始操作数
                    if pressed in 'Ee':
                        self.text.set(self.text.get()+pressed) # 不行：因为没有擦除
                    elif pressed == '\r':
                        self.onEval()                          # 回车键=eval
                    elif pressed == ' ':
                        self.onClear()                         # 空格键=清除
                    elif pressed == '\b':
                        self.text.set(self.text.get()[:-1])    # 退格键
                    elif pressed == '?':
                        self.help()

    def onHist(self):
        # 显示最近的计算器日志弹出窗口
        from tkinter.scrolledtext import ScrolledText
        new = Toplevel()                                  # 创建新窗口
        ok = Button(new, text="OK", command=new.destroy)
        ok.pack(pady=1, side=BOTTOM)                      # 先包装的越是后面才被裁剪
        text = ScrolledText(new, bg='beige')              # 添加Text和滚动条
        text.insert('0.0', self.eval.getHist())           # 从Evaluator获取文本
        text.see(END)                                     # 滚动到底
        text.pack(expand=YES, fill=BOTH)

        # 新窗口单击ok或者回车键后关闭
        new.title("PyCalc History")
        new.bind("<Return>", (lambda event: new.destroy()))
        # 下面这三步是模态化必不可少的操作，加深一下记忆
        ok.focus_set()                                  # 使用新的模态窗口
        new.grab_set()                                  # 获取键盘焦点
        new.wait_window()                               # 在new.destory后返回

    def help(self):
        self.infobox('PyCalc', 'PyCalc 3.0+\n'
                               'A Python/tkinter calculator\n'
                               'Programming Python 4E\n'
                               'May, 2010\n'
                               '(3.0 2005, 2.0 1999, 1.0 1996)\n\n'
                               'Use mouse or keyboard to\n'
                               'input numbers and operators,\n'
                               'or type code in cmd popup')


################################################################################
# 表达式求值类
# 嵌入并被CalcGui实例使用以完成计算
################################################################################

class Evaluator:
    """
    这个类管理两个堆栈。
    一个堆栈记录待处理的操作符（比如 +），另一个堆栈记录待处理的操作数（比如3.14）。
    在CalcGui发送来新的操作符时运算出临时结果并推送到操作数堆栈里。
    """

    # 重要的afterMe!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # afterMe这个字典很关键，把所有常用的数学操作符都列了一个比本身优先级低的集合，每当遇到下一个操作符比之前的优先级低的时候，则开始计算之前的值
    afterMe = {'*': ['+', '-', '(', '='],  # 类成员
               '/': ['+', '-', '(', '='],  # 遇到后不弹出的操作符
               '+': ['(', '='],  # 如果前一个操作符属于这些：push入栈
               '-': ['(', '='],  # 否则：pop弹出或求值前一个操作符
               ')': ['(', '='],  # 所有都遵循左结合原则
               '=': ['(']}

    def __init__(self):
        self.names = {}                         # 我的vars的命名空间
        self.opnd, self.optr = [], []           # 两个空的堆栈
        self.hist = []                          # 之前的历史日志
        self.runstring("from math import *")    # 提前导入math模块
        self.runstring("from random import *")  # 导入计算器的命名空间

    def clear(self):
        self.opnd, self.optr = [], []           # 保持名称完整
        if len(self.hist) > 64:                 # 避免历史显示过长
            self.hist = ['clear']
        else:
            self.hist.append('--clear--')

    def popOpnd(self):
        value = self.opnd[-1]                   # 弹出/返回顶部的|最近加入的操作数，也就是栈顶
        self.opnd[-1:] = []                     # 显示并切换到下一个
        return value                            # 也可以 x.pop(), 或者 del x[-1]

    def topOpnd(self):
        return self.opnd[-1]                    # 顶端的操作数（列表末尾）

    def open(self):
        self.optr.append('(')                   # 把 '(' 按操作符对待

    def close(self):                            # 输入')'后一直弹出到最上层的'(
        self.shiftOptr(')')                     # 括号里为空字符没问题：维持空字符状态
        self.optr[-2:] = []                     # 弹出，或者因为操作符再次添加

    def closeall(self):
        while self.optr:                        # 按下'eval'时对余下的强制操作
            self.reduce()                       # 最后那个可能是一个变量名
        try:
            self.opnd[0] = self.runstring(self.opnd[0])
        except:
            self.opnd[0] = '*ERROR*'            # 弹出，否则接下来再次添加

    def shiftOpnd(self, newopnd):               # 遇到操作符、')'和eval则推送操作数
        self.opnd.append(newopnd)

    def shiftOptr(self, newoptr):               # 应用<=级别的操作符
        while (self.optr and
               self.optr[-1] not in self.afterMe[newoptr]):
            self.reduce()
        self.optr.append(newoptr)               # 在结果中将这个操作符向上推送，操作符堆栈假定下一个操作数将被擦除

    def reduce(self):
        trace(self.optr, self.opnd)             # 折叠顶部的表达式
        try:
            operator       = self.optr[-1]      # 弹出顶端的操作符（列表末尾）
            [left, right]  = self.opnd[-2:]     # 弹出顶端的2个操作数（列表末尾）
            self.optr[-1:] = []                 # 原地删除切片
            self.opnd[-2:] = []
            result = self.runstring(left + operator + right)
            if result is None:
                result = left                   # 赋值? 键时变量名
            self.opnd.append(result)            # 把结果字符串推送回去给操作数堆栈
        except:
            self.opnd.append('*ERROR*')         # 堆栈/数字/名称错误

    def runstring(self, code):
        try:                                                  # 不再使用 `x`/repr
            result = str(eval(code, self.names, self.names))  # 尝试将表达式作为字符串处理
            self.hist.append(code + ' => ' + result)          # 添加到历史日志
        except:
            exec(code, self.names, self.names)                # eval执行错误的让exec来尝试一下，其实这里我觉得有点怪
            self.hist.append(code)
            result = None                                     # 并且不管三七二十一把result设为None
        return result

    def getHist(self):
        """显示计算历史"""
        return '\n'.join(self.hist)


def getCalcArgs():
    from sys import argv                                        # 获取放在字典里的命令行参数
    config = {}                                                 # 例如： -bg black -fg red
    for arg in argv[1:]:                                        # 尚未支持字体
        if arg in ['-bg', '-fg']:                               # -bg red' -> {'bg':'red'}
            try:
                config[arg[1:]] = argv[argv.index(arg) + 1]
            except:
                pass
    return config


if __name__ == '__main__':
    CalcGui(**getCalcArgs()).mainloop()                         # 在默认顶级窗口中
